/*
 *
 *  * Unidata Platform
 *  * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *  *
 *  * Commercial License
 *  * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *  *
 *  * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 *  * For clarification or additional options, please contact: info@unidata-platform.com
 *  * -------
 *  * Disclaimer:
 *  * -------
 *  * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 *  * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 *  * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 *  * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 *  * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 *
 */

package org.unidata.mdm.data.service.segments.relations.draft;

import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.calculables.CalculableHolder;
import org.unidata.mdm.core.type.data.RecordStatus;
import org.unidata.mdm.core.type.timeline.MutableTimeInterval;
import org.unidata.mdm.core.type.timeline.TimeInterval;
import org.unidata.mdm.core.type.timeline.Timeline;
import org.unidata.mdm.core.type.timeline.impl.RevisionSlider;
import org.unidata.mdm.data.context.DeleteRelationRequestContext;
import org.unidata.mdm.data.context.DeleteRelationRequestContext.DeleteRelationHint;
import org.unidata.mdm.data.context.GetRelationRequestContext;
import org.unidata.mdm.data.context.RelationRestoreContext.RestoreRelationHint;
import org.unidata.mdm.data.context.RestoreFromRelationRequestContext;
import org.unidata.mdm.data.context.RestoreToRelationRequestContext;
import org.unidata.mdm.data.context.UpsertRelationRequestContext;
import org.unidata.mdm.data.context.UpsertRelationRequestContext.UpsertRelationHint;
import org.unidata.mdm.data.context.UpsertRelationsRequestContext;
import org.unidata.mdm.data.exception.DataExceptionIds;
import org.unidata.mdm.data.module.DataModule;
import org.unidata.mdm.data.service.DataRelationsService;
import org.unidata.mdm.data.service.impl.CommonRelationsComponent;
import org.unidata.mdm.data.service.segments.RelationDraftTimelineSupport;
import org.unidata.mdm.data.type.data.OriginRelation;
import org.unidata.mdm.data.type.data.RelationType;
import org.unidata.mdm.data.type.draft.DataDraftConstants;
import org.unidata.mdm.data.type.draft.DataDraftOperation;
import org.unidata.mdm.data.type.keys.RelationKeys;
import org.unidata.mdm.draft.context.DraftPublishContext;
import org.unidata.mdm.draft.context.DraftQueryContext;
import org.unidata.mdm.draft.dto.DraftPublishResult;
import org.unidata.mdm.draft.dto.DraftQueryResult;
import org.unidata.mdm.draft.exception.DraftProcessingException;
import org.unidata.mdm.draft.service.DraftService;
import org.unidata.mdm.draft.type.Draft;
import org.unidata.mdm.draft.type.Edition;
import org.unidata.mdm.meta.type.RelativeDirection;
import org.unidata.mdm.system.type.pipeline.Finish;
import org.unidata.mdm.system.type.pipeline.Start;
import org.unidata.mdm.system.type.support.IdentityHashSet;

/**
 * @author Alexey Tsarapkin
 */
@Component(RelationDraftPublishFinishExecutor.SEGMENT_ID)
public class RelationDraftPublishFinishExecutor extends Finish<DraftPublishContext, DraftPublishResult>
    implements RelationDraftTimelineSupport {
    /**
     * This segment ID.
     */
    public static final String SEGMENT_ID = DataModule.MODULE_ID + "[RELATION_DRAFT_PUBLISH_FINISH]";
    /**
     * This segment description.
     */
    private static final String SEGMENT_DESCRIPTION = DataModule.MODULE_ID + ".relation.draft.publish.finish.description";
    /**
     * PC.
     */
    @Autowired
    private DataRelationsService dataRelationsService;
    /**
     * CRC.
     */
    @Autowired
    private CommonRelationsComponent commonRelationsComponent;
    /**
     * MMS.
     */
    @Autowired
    private MetaModelService metaModelService;
    /**
     * DS.
     */
    @Autowired
    private DraftService draftService;
    /**
     * Constructor.
     */
    public RelationDraftPublishFinishExecutor(){
        super(SEGMENT_ID, SEGMENT_DESCRIPTION, DraftPublishResult.class);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public DraftPublishResult finish(DraftPublishContext ctx) {

        Draft draft = ctx.currentDraft();
        Edition edition = ctx.currentEdition();

        RelationType type = draft
                .getVariables()
                .valueGet(DataDraftConstants.RELATION_TYPE, RelationType.class);

        DraftPublishResult result = new DraftPublishResult(publish(ctx, draft, edition));
        result.setDraft(draft);
        result.setStop(type == RelationType.CONTAINS);

        return result;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean supports(Start<?, ?> start) {
        return DraftPublishContext.class.isAssignableFrom(start.getInputTypeClass());
    }
    /**
     * Does publish periods to persistent storage.
     * @param ctx the incoming context
     * @param draft the context
     * @param edition current edition
     * @return true, if successful
     */
    private boolean publish(DraftPublishContext ctx, Draft draft, Edition edition) {

        DataDraftOperation operation = draft.getVariables().valueGet(DataDraftConstants.OPERATION_CODE, DataDraftOperation.class);
        Objects.requireNonNull(operation, "Initial operation must not be null!");

        Timeline<OriginRelation> timeline = timeline(draft, edition);
        RelationKeys next = timeline.getKeys();
        RelationKeys current = next.isNew() ? null : commonRelationsComponent.identify(GetRelationRequestContext.builder()
                .relationEtalonKey(next.getEtalonKey().getId())
                .build());

        Set<CalculableHolder<OriginRelation>> supply
            = new TreeSet<>(RevisionSlider.POP_TOP_COMPARATOR);

        supply.addAll(timeline.stream()
            .map(TimeInterval::unlock)
            .map(MutableTimeInterval::toModifications)
            .map(Map::values)
            .flatMap(Collection::stream)
            .flatMap(Collection::stream)
            .collect(Collectors.toCollection(IdentityHashSet::new)));

        switch (operation) {
        case DELETE_RECORD:
            publishInactiveState(ctx, draft, current, next, supply);
            break;
        case DELETE_PERIOD:
        case RESTORE_RECORD:
        case RESTORE_PERIOD:
        case UPSERT_DATA:
            publishActiveState(ctx, draft, current, next, supply);
            break;
        default:
            break;
        }

        return true;
    }

    private void publishActiveState(DraftPublishContext ctx, Draft draft, RelationKeys current, RelationKeys next,
            Collection<CalculableHolder<OriginRelation>> holders) {

        // 1. Process containments differently than other relations.
        if (next.getRelationType() == RelationType.CONTAINS) {
            publishContainmentActiveState(ctx, draft, current, next);
            return;
        }

        // 2. If the record is in INACTIVE state - activate it
        if (current != null && !current.isActive()) {
            doRestoreRelation(draft, next);
        }

        // 3. Default tree set ascending iterator
        Iterator<CalculableHolder<OriginRelation>> it = holders.iterator();
        while (it.hasNext()) {

            CalculableHolder<OriginRelation> ch = it.next();

            // 3.1. Do the upsert
            doUpsertPeriod(ch, next);

            // 3.2. Inactivate period, if such a one is detected
            if (ch.getStatus() == RecordStatus.INACTIVE) {
                doInactivatePeriod(ch, next);
            }
        }
    }

    private void publishInactiveState(DraftPublishContext ctx, Draft draft, RelationKeys current,
            RelationKeys next, Collection<CalculableHolder<OriginRelation>> holders) {

        // 1. Process containments differently than other relations.
        if (next.getRelationType() == RelationType.CONTAINS) {
            publishContainmentInactiveState(ctx, draft, current, next);
            return;
        }

        // 2. Invalid state. Skip silently.
        if ((current == null || !current.isActive()) && CollectionUtils.isEmpty(holders)) {
            return;
        }

        // 3. Put data, if needed
        if (CollectionUtils.isNotEmpty(holders)) {
            publishActiveState(ctx, draft, current, next, holders);
        }

        // 4. Finally deactivate record.
        doInactivateRelation(next);
    }

    private void publishContainmentActiveState(DraftPublishContext ctx, Draft draft, RelationKeys current, RelationKeys next) {

        // 1. Child
        publishContainmentDraft(ctx, draft, next);

        // 2. If the record is in INACTIVE state - activate it
        if (current != null && !current.isActive()) {
            doRestoreRelation(draft, next);
        } else {

            // 3. Simulate upsert, just to recalculate state (index changes) or create in the case the record is new.
            UpsertRelationsRequestContext uCtx = UpsertRelationsRequestContext.builder()
                .relationFrom(next.getRelationName(), UpsertRelationRequestContext.builder()
                        .record(null)
                        .etalonKey(next.getEtalonKey().getTo())
                        .originKey(next.getOriginKey().getTo())
                        .hint(UpsertRelationHint.HINT_ETALON_ID, next.getEtalonKey().getId())
                        .hint(UpsertRelationHint.HINT_PUBLISHING, Boolean.TRUE)
                        .build())
                .originKey(next.getOriginKey().getFrom())
                .etalonKey(next.getEtalonKey().getFrom())
                .build();

            dataRelationsService.upsertRelations(uCtx);
        }
    }

    private void publishContainmentInactiveState(DraftPublishContext ctx, Draft draft, RelationKeys current, RelationKeys next) {

        // 1. Invalid state. Skip silently.
        boolean wasActive = BooleanUtils.isTrue(draft.getVariables().valueGet(DataDraftConstants.DRAFT_HAD_ACTIVE_STATE));
        if ((current == null || !current.isActive()) && !wasActive) {
            return;
        }

        // 2. Create and inactivate or just insactivate
        if (current == null) {

            publishContainmentDraft(ctx, draft, next);

            // 2.1. Simulate upsert, just to create in the case the record is new.
            UpsertRelationsRequestContext uCtx = UpsertRelationsRequestContext.builder()
                .relationFrom(next.getRelationName(), UpsertRelationRequestContext.builder()
                        .record(null)
                        .etalonKey(next.getEtalonKey().getTo())
                        .originKey(next.getOriginKey().getTo())
                        .hint(UpsertRelationHint.HINT_ETALON_ID, next.getEtalonKey().getId())
                        .hint(UpsertRelationHint.HINT_PUBLISHING, Boolean.TRUE)
                        .build())
                .originKey(next.getOriginKey().getFrom())
                .etalonKey(next.getEtalonKey().getFrom())
                .build();

            dataRelationsService.upsertRelations(uCtx);

            doInactivateRelation(next);
        // 3. Turn off the relation itself to successfully bypass consistency checks on records
        } else if (current.isActive()) {
            doInactivateRelation(next);
        }

        // 4. Put data, if needed
        if (current != null) {
            publishContainmentDraft(ctx, draft, next);
        }
    }

    private void publishContainmentDraft(DraftPublishContext ctx, Draft draft, RelationKeys next) {

        DraftQueryResult containment = draftService.drafts(DraftQueryContext.builder()
                .parentDraftId(draft.getDraftId())
                .subjectId(next.getEtalonKey().getTo().getId())
                .build());

        if (!containment.hasDrafts() || containment.getDrafts().size() != 1) {
            throwContainmentDraftNotFound(draft, next);
        }

        Draft child = containment.getDrafts().get(0);
        draftService.publish(DraftPublishContext.builder()
                .draftId(child.getDraftId())
                .parentDraftId(draft.getDraftId())
                .delete(ctx.isDelete())
                .force(ctx.isForce())
                .operationId(ctx.getOperationId())
                .build());
    }

    private void doInactivateRelation(RelationKeys next) {

        DeleteRelationRequestContext dCtx = DeleteRelationRequestContext.builder()
                .relationEtalonKey(next.getEtalonKey().getId())
                .inactivateEtalon(true)
                .hint(DeleteRelationHint.HINT_PUBLISHING, Boolean.TRUE)
                .build();

        dataRelationsService.deleteRelation(dCtx);
    }

    private void doRestoreRelation(Draft draft, RelationKeys next) {

        RelativeDirection direction = draft
                .getVariables()
                .valueGet(DataDraftConstants.RELATION_DIRECTION, RelativeDirection.class);

        if (direction == RelativeDirection.FROM) {

            RestoreFromRelationRequestContext rCtx = RestoreFromRelationRequestContext.builder()
                    .relationEtalonKey(next.getEtalonKey().getId())
                    .hint(RestoreRelationHint.HINT_PUBLISHING, Boolean.TRUE)
                    .build();

            dataRelationsService.restore(rCtx);
        } else {

            RestoreToRelationRequestContext rCtx = RestoreToRelationRequestContext.builder()
                    .relationEtalonKey(next.getEtalonKey().getId())
                    .hint(RestoreRelationHint.HINT_PUBLISHING, Boolean.TRUE)
                    .build();

            dataRelationsService.restore(rCtx);
        }
    }

    private void doInactivatePeriod(CalculableHolder<OriginRelation> ch, RelationKeys next) {

        DeleteRelationRequestContext dCtx = DeleteRelationRequestContext.builder()
                .relationEtalonKey(next.getEtalonKey().getId())
                .inactivatePeriod(true)
                .validFrom(ch.getValidFrom())
                .validTo(ch.getValidTo())
                .build();

        dataRelationsService.deleteRelation(dCtx);
    }

    private void doUpsertPeriod(CalculableHolder<OriginRelation> ch, RelationKeys next) {

        UpsertRelationsRequestContext uCtx = UpsertRelationsRequestContext.builder()
            .relationFrom(next.getRelationName(), UpsertRelationRequestContext.builder()
                    .record(ch.getValue())
                    .etalonKey(next.getEtalonKey().getTo())
                    .externalId(next.getOriginKey().getTo().toExternalId())
                    .validFrom(ch.getValidFrom())
                    .validTo(ch.getValidTo())
                    .hint(UpsertRelationHint.HINT_ETALON_ID, next.getEtalonKey().getId())
                    .hint(UpsertRelationHint.HINT_PUBLISHING, Boolean.TRUE)
                    .build())
            .originKey(next.getOriginKey().getFrom())
            .etalonKey(next.getEtalonKey().getFrom())
            .build();

        dataRelationsService.upsertRelations(uCtx);
    }

    private void throwContainmentDraftNotFound(Draft draft, RelationKeys next) {
        throw new DraftProcessingException("Cannot publish relation. Containment draft for parent [{}] and subject ID [{}] not found.",
                DataExceptionIds.EX_DATA_RELATION_DRAFT_CONTAINMENT_DRAFT_NOT_FOUND,
                draft.getDraftId(), next.getEtalonKey().getTo().getId());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CommonRelationsComponent getCommonRelationsComponent() {
        return commonRelationsComponent;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public MetaModelService getMetaModelService() {
        return metaModelService;
    }
}
